回顧一下我們目前完成的效果:
完成了新增待辦事項的功能之後,接下來當然是要來能夠註記是否已完成該項待辦事項囉!如果你有操作的話其實會發現,目前清單前面的 Checkbox 是可以打勾的,但實質上並無真正的註記效果。
怎麼說呢?我們可以到原本 TodoMVC 的頁面操作看看就知道了。
發現了嗎?當我們將待辦事項前的 checkbox 打勾之後,待辦事項其實應該會被畫上刪除線,且字體顏色應該要變淡。
我們先來做這個功能吧!這個功能說起來其實滿簡單的,只要在 checkbox 打勾之後,在元素上多加一個 class 就能完成。
只不過我們目前的資料結構並不足以完成這件事,因為我們目前只有將待辦事項的事項名稱記下來而已,並沒有記錄該事項是否已完成。
所以我們先從資料開始下手,先建立一個叫做 Todo 的資料物件模型:
ng generate class todo-list/todo --type model
在新增類別的指令額外加上
--type
的參數是為了要讓 CLI 幫我們產生檔案時,檔案名稱會變成[filename].[type].ts
這樣的命名方式,檔案內容並不會有任何變化。
如果沒有加上--type
的話,CLI 在產生檔案時,檔案名稱只會是[filename].ts
而已。
這時候 CLI 就會建立一個名為 todo.model.ts
的檔案,裡面大概長這樣:
/**
* 待辦事項的資料物件模型
*
* @export
* @class Todo
*/
export class Todo {
}
接著新增兩個私有屬性,用以記錄待辦事項的事項名稱以及完成與否:
export class Todo {
/**
* 事項名稱
*
* @private
* @memberof Todo
*/
private title = '';
/**
* 完成與否
*
* @private
* @memberof Todo
*/
private completed = false;
}
我個人習慣在宣告屬性時就先給預設值,一方面是讓 VSCode 知道這個變數的資料型態是什麼;一方面是減少預期之外的行為。
至於屬性宣告為私有屬性是因為我不希望外部可以隨便修改這裡面的值,如果你不介意,可以不用加上private
的宣告就是公有的囉!
再來是覆寫一下建構式:
/**
* Creates an instance of Todo.
*
* @param {string} title - 待辦事項的名稱
* @memberof Todo
*/
constructor(title: string) {
this.title = title || ''; // 為避免傳入的值為 Falsy 值,稍作處理
}
由於屬性宣告為私有屬性,所以寫個 getter 讓外部可以取得該屬性:
/**
* 此事項是否已經完成
*
* @readonly
* @type {boolean}
* @memberof Todo
*/
get done(): boolean {
return this.completed;
}
/**
* 取得事項名稱
*
* @returns {string}
* @memberof Todo
*/
getTitle(): string {
return this.title;
}
在這裡我特意使用兩種方式來讓外部可以取得內部私有的屬性,稍後可以稍微留意一下使用方式,比較一下差別。
除此之外,由於是 checkbox 的關係,可以打勾之後再取消打勾,所以我們再加上一個 toggleCompletion
的函式來讓外部可以使用:
/**
* 來回切換完成狀態
*
* @memberof Todo
*/
toggleCompletion(): void {
this.completed = !this.completed;
}
資料物件模型做好了之後,我們來調整一下原本的程式碼,先開啟 todo-list.service.ts
,將程式碼調整成像是這樣:
import { Injectable } from '@angular/core';
// Class
import { Todo } from './todo.model';
@Injectable({
providedIn: 'root'
})
export class TodoListService {
private list: Todo[] = [];
constructor() { }
/**
* 取得待辦事項清單
*
* @returns {Todo[]}
* @memberof TodoListService
*/
getList(): Todo[] {
return this.list;
}
/**
* 新增待辦事項
*
* @param {string} title - 待辦事項的標題
* @memberof TodoListService
*/
add(title: string): void {
// 避免傳入的 title 是無效值或空白字串,稍微判斷一下
if (title || title.trim()) {
this.list.push(new Todo(title));
}
}
}
接著是 todo-list.component.ts
:
import { Component, OnInit } from '@angular/core';
// Service
import { TodoListService } from './todo-list.service';
// Class
import { Todo } from './todo.model';
@Component({
selector: 'app-todo-list',
templateUrl: './todo-list.component.html',
styleUrls: ['./todo-list.component.css']
})
export class TodoListComponent implements OnInit {
constructor(private todoListService: TodoListService) { }
ngOnInit() {
}
/**
* 新增代辦事項
*
* @param {HTMLInputElement} inputRef - 輸入框的元素實體
* @memberof TodoListComponent
*/
addTodo(inputRef: HTMLInputElement): void {
const todo = inputRef.value.trim();
if (todo) {
this.todoListService.add(todo);
inputRef.value = '';
}
}
/**
* 取得待辦事項清單
*
* @returns {Todo[]}
* @memberof TodoListComponent
*/
getList(): Todo[] {
return this.todoListService.getList();
}
}
最後則是將 todo-list.component.html
裡的 {{ todo }}
改為 {{ todo.getTitle() }}
。
到目前為止,我們只是重構了一下程式碼,所以先來測試功能有沒有壞掉。
如果有遇到問題都可以在下方留言給我噢!
好!功能都正常之後,我們再把資料綁到 Template 上:
<ul class="todo-list">
<li
*ngFor="let todo of getList(); let i = index"
[class.completed]="todo.done"
>
<div class="view">
<input
class="toggle"
type="checkbox"
(click)="todo.toggleCompletion()"
[checked]="todo.done"
>
<label>{{ todo.getTitle() }}</label>
<button class="destroy"></button>
</div>
</li>
</ul>
有注意到
done
跟toggleCompletion
明明都是函式,卻有著不一樣的使用方式嗎?
來看看效果:
很好,完成功能且運作正常!
接下來我們希望能夠可以有刪除的功能,所以我們打開 todo-list.service.ts
實作刪除的函式:
/**
* 移除待辦事項
*
* @param {number} index - 待辦事項的索引位置
* @memberof TodoListService
*/
remove(index: number): void {
this.list.splice(index, 1);
}
再來是 todo-list.component.ts
:
/**
* 移除待辦事項
*
* @param {number} index - 待辦事項的索引位置
* @memberof TodoListComponent
*/
remove(index: number): void {
this.todoListService.remove(index);
}
最後在按鈕上加上事件的綁定:
<button
class="destroy"
(click)="remove(i)"
></button>
再來看一下效果:
很好!刪除的功能也順利完成了!
今天就先到這邊,稍微吸收一下,我們明天再來把功能做得更完善吧!
明天見!
jakeuj
與 jacky123200
的提醒,補上漏掉的 let i = index;
語法。Leo大大 小弟有個關於刪除功能的問題
目前在做練習 仿照上文的刪除鍵做法 請問Leo大大有看出問題嗎
Hi Jackson,
有阿,
首先是在 app.component.ts
的 remove
函式裡,參數 index
一定是 undefined,因為那需要把 app.component.html
的 Line 21 改成這樣才能正確傳入索引值:
<ng-container *ngFor="let message of messages; let i = index">
再來就是你想問的問題,remove 這個函式本身就不是陣列的函式,要馬使用陣列的原生函式處理,要馬就是自己做函式處理(但也會用到原生函式)
了解 感謝你的回答
另外想問 type="reset"的部分
延續上圖練習 我將"清空所有項目"鍵設type="reset"能把 from裡面的資料清空
但若將type ="reset"放在刪除鍵卻無法 請問為何?
Hi Jackson,
那是因為我們已經寫好 function 去處理啦~~
所以雖然都是 type="reset"
,但實際做的事情還是要看我們是怎麼去處理。
文中
*ngFor="let todo of getList()
少了
;let i = index
Hi Leo大大:
我不管打甚麼ng的指令都會出現
"ng : 無法辨識 'ng' 詞彙是否為 Cmdlet、函數、指令檔或可執行程式的名稱。請檢查名稱拼字是否正確,如果包含路徑的話,請確認路徑是
否正確,然後再試一次。"
這個error訊息,請問該怎麼解決
Hi linsslinss2004,
這通常是因為:
以上只是我的猜測,你可以檢查看看~
雖然文中
*ngFor="let todo of getList()
少了
;let i = index
但是為什麼在這可以讀取到i呢? (click)="remove(i)"
另外一個問題是
get done(): boolean {
return this.completed;
}
為什麼get 和done()是分開的?
這個是什麼語法?
謝謝解答
Hi jacky123200,
非常感謝你的提醒,原來文章裡漏掉了這段語法,而且之前已經有邦友提醒過了,我居然還沒有改到!!
那個 get
是一種宣告,表示接下來的這個函式是一個 getter
,這種函式在使用時,不用像一般函式那樣需要加上 ()
才能執行。
例如:
get done(): boolean {
return this.completed;
}
console.log(this.done);
// 與
done(): boolean {
return this.completed;
}
console.log(this.done());
謝謝
[class.completed]="todo.done"
是什麼 ? 看起來不像 property binding
one way binding 除了 event binding 和 插值, 其他好像不能傳入方法 ?
(click)="todo.toggleCompletion()"
[checked]="todo.done"
可以用像是 [(ngModel)]="todo.toggleCompletion()"
的形式嗎 ?
上面那個和 [ngModel]="todo.toggleCompletion()"
都失敗
Hi obelisk0114,
[]
包起來的基本都算是 property binding 。[ngModel]
or [(ngModel)]
要在 Module 裡 import FormsModule
。[class.completed]="todo.done"
是什麼意思 ?
2. 從 Angular 的官網有這句
Similarly, you cannot use property binding to call a method on the target element.
https://angular.io/guide/template-syntax#property-binding-property
但是 [checked]="todo.done"
沒有報錯, 所以看起來只要是單純取值就可以 ?
3. import FormsModule 還是會失敗
Hi obelisk0114,
<li [class.completed]="todo.done"></li>
<!-- todo.done 為 true 時 -->
<li class="completed"></li>
<!-- todo.done 為 false 時 -->
<li></li>
在 Angular 裡, []
是 input 、 ()
是 output , [(ngModel)]
是雙向綁定,表示其同時具有 input 以及 output 的功能,而且這個 output 還是直接會把值塞回去的那種。
所以你覺得 todo.toggleCompletion()
的回傳結果是可以被指定值的嗎?
也就是說 [(ngModel)]
只能用在 component 的 property, 不能用在 <input>
的 checked
(checked
和 !completed
使用 two-way binding) ?
不,我指的是,要直接給它變數,它才能將值指定給該變數
後來把 private completed
的 private
移除,可以用 [(ngModel)]
做出原來的效果。我之前搞錯了,感謝大大的說明
很高興能夠幫得上忙^^
請教一下動態gif檔是用哪一款程式呢? 真的方便易懂,謝謝
Hi messboy000,
就只是螢幕錄影之後,在線上找個影片轉gif檔的網站而已 :)
Hi Leo 大大:
想請問這段是什麼意思呢?
constructor(title: string) {
this.title = title || ''; // 為避免傳入的值為 Falsy 值,稍作處理
}
this.title 等於title 或者''嗎?
什麼時候會等於''呢? 當沒有title的時候嗎?
改成以下這個,好像也沒有出現什麼錯誤
constructor(title: string) {
this.title = title;
}
先謝謝大大地回答:)
Hi smile98,
this.title 等於title 或者''嗎?
是的沒錯。
什麼時候會等於''呢? 當沒有title的時候嗎?
是的,當傳入的 title
值為 falsy
的時候,就會等於 ''
。
關於什麼是 falsy
,原文內有連結,請參考。
改成以下這個,好像也沒有出現什麼錯誤
constructor(title: string) {
this.title = title;
}
這就要看你對於錯誤定義囉,基本上如果用 new Todo(0)
、new Todo(undefined)
、new Todo(null)
這三個方式去建立這個 todo ,就可能會造成錯誤。
基本上,這樣寫就是個防呆機制,可做可不做,自由心證囉。
Leo 大您好:
感謝您撰文分享,內容非常清楚,十分受用
但let i = index
的地方似乎還沒補上XD"
如果是我眼拙漏看的話先說聲抱歉!!
我搜尋了一下是有找到,還是你是指有別的地方漏掉了呢?
Leo大大您好,我跟著完成後,我沒有可以按叉叉的button耶,我的html是也有跟著您刻出來壓~~
可能是因為 CSS 的原因?我在上一篇有回你了,你把它補上試試?
Leo大大~~我兩邊都有加惹你貼給我的那段,但還是都沒有叉叉可以給我用,我也有把checkbox勾選起來QQ
Leo大大我找到問題惹,是我class打錯,所以沒有吃到css,謝謝你我要繼續看你的文章惹~~
嗯嗯,加油加油!